/*
 * The org.opensourcephysics.media.core package defines the Open Source Physics
 * media framework for working with video and other media.
 *
 * Copyright (c) 2004  Douglas Brown and Wolfgang Christian.
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * For additional information and documentation on Open Source Physics,
 * please see <http://www.opensourcephysics.org/>.
 */
package org.opensourcephysics.media.core;

import java.beans.*;
import java.io.*;
import java.util.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;

import javax.swing.SwingUtilities;

import org.opensourcephysics.display.*;
import org.opensourcephysics.controls.*;

/**
 * This is an interactive drawing panel with a video player.
 * It can draw videos and other Trackable objects in either
 * imagespace or worldspace. When drawing in imagespace, the image
 * reference frame (ie the image itself) is fixed. When
 * drawing in worldspace, the world reference frame is fixed.
 * The image reference frame defines positions in pixel units
 * relative to the upper left corner of a video image--ie, the UL
 * corner of a 320 x 240 video is at (0.0, 0.0) and the LR corner
 * is at (320.0, 240.0). When drawing in imagespace, non-Trackable
 * objects are not drawn.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class VideoPanel extends InteractivePanel
                        implements PropertyChangeListener {

  // static fields
  protected static int defaultWidth = 640;
  protected static int defaultHeight = 480;

  // instance fields
  protected VideoPlayer player;
  protected TextPanel mousePanel;
  protected TextPanel messagePanel;
  protected Video video = null;
  protected boolean playerVisible = true;
  protected boolean drawingInImageSpace = false;
  protected double imageWidth, imageHeight;  // image dimensions in image units
  protected double xOffset, yOffset;  // imagespace drawing offset in pixel units
  protected double imageBorder;  // a fraction >= 0
  protected ImageCoordSystem coords; // image <--> world transforms
  protected Point2D pt = new Point2D.Double();
  protected File dataFile;
  protected Map filterClasses = new TreeMap(); // maps filter names to classes
  public boolean changed = false;
  public String defaultFileName;

  /**
   * Constructs a blank VideoPanel with a player.
   */
  public VideoPanel() {
    this(null);
  }

  /**
   * Constructs a VideoPanel with a video and player.
   *
   * @param video the video to be drawn
   */
  public VideoPanel(Video video) {
    setSquareAspect(true);
    player = new VideoPlayer(this);
    player.addPropertyChangeListener("videoclip", this); //$NON-NLS-1$
    player.addPropertyChangeListener("stepnumber", this); //$NON-NLS-1$
    player.addPropertyChangeListener("frameduration", this); //$NON-NLS-1$
    add(player, BorderLayout.SOUTH);
    VideoClip clip = player.getVideoClip();
    clip.addPropertyChangeListener("startframe", this); //$NON-NLS-1$
    clip.addPropertyChangeListener("stepsize", this); //$NON-NLS-1$
    clip.addPropertyChangeListener("stepcount", this); //$NON-NLS-1$
    clip.addPropertyChangeListener("starttime", this); //$NON-NLS-1$
    // define mousePanel and messagePanel
    mousePanel = blMessageBox;
    messagePanel = brMessageBox;
    // make new CoordinateStringBuilder
    setCoordinateStringBuilder(new VidCartesianCoordinateStringBuilder());
    // create coords and put origin at center of panel
    coords = new ImageCoordSystem();
    setVideo(video);
    if (video != null && video.getImage().getWidth() > 0) {
      setImageWidth(video.getImage().getWidth());
      setImageHeight(video.getImage().getHeight());
    }
    else {
      setImageWidth(defaultWidth);
      setImageHeight(defaultHeight);
    }
    int w = (int)getImageWidth();
    int h = (int)getImageHeight();
    setPreferredSize(new Dimension(w, h + player.height));
    // put origin at center of image
    coords.setAllOriginsXY(imageWidth/2, imageHeight/2);
    addFocusListener(new FocusAdapter() {
      public void focusLost(FocusEvent e) {
        hideMouseBox();
      }
    });
  }

  /**
   * Sets the video.
   *
   * @param newVideo the video
   */
  public void setVideo(Video newVideo) {
    if (newVideo == video) return;
    getPlayer().setVideoClip(new VideoClip(newVideo));
  }

  /**
   * Gets the video.
   *
   * @return the video
   */
  public Video getVideo() {
    return video;
  }

  /**
   * Gets the image width in image units.
   *
   * @return the width
   */
  public double getImageWidth() {
    return imageWidth;
  }

  /**
   * Sets the image width in image units.
   *
   * @param w the width
   */
  public void setImageWidth(double w) {
    // don't allow widths smaller than the video
    if (video != null) {
      BufferedImage vidImage = video.getImage();
      if (vidImage != null) w = Math.max(w, vidImage.getWidth());
    }
    imageWidth = w;
  }

  /**
   * Gets the image height in image units (1.0 unit/pixel).
   *
   * @return the height
   */
  public double getImageHeight() {
    return imageHeight;
  }

  /**
   * Sets the image height in image units (1.0 unit/pixel).
   *
   * @param h the height
   */
  public void setImageHeight(double h) {
    // don't allow heights smaller than the video
    if (video != null) {
      BufferedImage vidImage = video.getImage();
      if (vidImage != null) h = Math.max(h, vidImage.getHeight());
    }
    imageHeight = h;
  }

  /**
   * Gets the image border.
   *
   * @return the border fraction
   */
  public double getImageBorder() {
    return imageBorder;
  }

  /**
   * Sets the image border.
   *
   * @param borderFraction the border fraction
   */
  public void setImageBorder(double borderFraction) {
    imageBorder = Math.max(borderFraction, 0);
  }

  /**
   * Sets the image coordinate system used to convert between
   * image and world spaces.
   *
   * @param newCoords the image coordinate system
   */
  public void setCoords(ImageCoordSystem newCoords) {
    if (video != null) video.setCoords(newCoords);
    else coords = newCoords;
  }

  /**
   * Gets the current image coordinate system used for drawing.
   *
   * @return the current image coordinate system
   */
  public ImageCoordSystem getCoords() {
    return coords;
  }

  /**
   * Sets the file in which data is saved.
   *
   * @param file the data file
   */
  public void setDataFile(File file) {
    File prev = dataFile;
    dataFile = file;
    if (file != null) defaultFileName = XML.forwardSlash(file.getName());
    firePropertyChange("datafile", prev, dataFile); //$NON-NLS-1$
    OSPLog.fine("Data file: " + file); //$NON-NLS-1$
  }

  /**
   * Gets the file where data is saved.
   *
   * @return the data file
   */
  public File getDataFile() {
    return dataFile;
  }

  /**
   * Gets the default path for the saveAs method.
   *
   * @return the relative path to the file
   */
  public String getFilePath() {
    return defaultFileName;
  }

  /**
   * Sets the drawing space to imagespace or worldspace.
   *
   * @param imagespace <code>true</code> to draw in imagespace
   */
  public void setDrawingInImageSpace(boolean imagespace) {
    drawingInImageSpace = imagespace;
    if (imagespace) {
      setAutoscaleX(false);
      setAutoscaleY(false);
    }
    else {
      setAutoscaleX(true);
      setAutoscaleY(true);
    }
    firePropertyChange("imagespace", null, new Boolean(imagespace)); //$NON-NLS-1$
    repaint();
  }

  /**
   * Returns true if this is drawing in image space rather than
   * world space.
   *
   * @return <code>true</code> if drawing in image space
   */
  public boolean isDrawingInImageSpace() {
    return drawingInImageSpace;
  }

  /**
   * Gets the video player.
   *
   * @return the video player
   */
  public VideoPlayer getPlayer() {
    return player;
  }

  /**
   * Shows or hides the video player.
   *
   * @param visible <code>true</code> to show the player
   */
  public void setPlayerVisible(final boolean visible) {
    if (visible == playerVisible) return;
    Runnable setPlayerVis = new Runnable() {
      public void run() {
        playerVisible = visible;
        if (playerVisible) add(player, BorderLayout.SOUTH);
        else remove(player);
        repaint();
      }
    };
    SwingUtilities.invokeLater(setPlayerVis);
  }

  /**
   * Gets the video player visibility.
   *
   * @return <code>true</code> if the player is visible
   */
  public boolean isPlayerVisible() {
    return playerVisible;
  }

  /**
   * Gets the current step number.
   *
   * @return the current step number
   */
  public int getStepNumber() {
    return getPlayer().getStepNumber();
  }

  /**
   * Gets the current frame number.
   *
   * @return the frame number
   */
  public int getFrameNumber() {
    return getPlayer().getFrameNumber();
  }

  /**
   * Overrides DrawingPanel getDrawables method.
   *
   * @return a list of Drawable objects
   */
  public ArrayList getDrawables() {
    if (!isDrawingInImageSpace()) return super.getDrawables();
    return super.getDrawables(Trackable.class);
  }

  /**
   * Adds a drawable object to the drawable list.
   *
   * @param drawable the drawable object
   */
  public synchronized void addDrawable(Drawable drawable) {
    if (drawable == null) return;
    if (drawable instanceof Video) setVideo((Video)drawable);
    else super.addDrawable(drawable);
    repaint();
  }

  /**
   * Removes a drawable object from the drawable list.
   *
   * @param drawable the drawable object
   */
  public synchronized void removeDrawable(Drawable drawable) {
    if (drawable == video) setVideo(null);
    else super.removeDrawable(drawable);
  }

  /**
   * Removes all objects of the specified class.
   *
   * @param c the class to remove
   */
  public synchronized void removeObjectsOfClass(Class c) {
    if (video.getClass() == c) setVideo(null);
    else super.removeObjectsOfClass(c);
  }

  /**
   * Removes all drawable objects except the video.
   * To remove the video, use setVideo(null);
   */
  public synchronized void clear() {
    super.clear();
    // add back the video
    if (video != null) super.addDrawable(video);
  }

  /**
   * Adds a video filter class to the map of available filters.
   *
   * @param filterClass the filter class to add
   */
  public void addFilter(Class filterClass) {
    if (Filter.class.isAssignableFrom(filterClass)) {
      String name = filterClass.getName();
      filterClasses.put(name, filterClass);
      firePropertyChange("filterClass", null, filterClass); //$NON-NLS-1$
    }
  }

  /**
   * Removes a video filter class from the map of available filters.
   *
   * @param filterClass the filter class to remove
   */
  public void removeFilter(Class filterClass) {
    if (Filter.class.isAssignableFrom(filterClass)) {
      String name = filterClass.getName();
      Object obj = filterClasses.remove(name);
      if (obj != null) {
        firePropertyChange("filterClass", filterClass, null); //$NON-NLS-1$
      }
    }
  }

  /**
   * Gets the map of available video filters.
   *
   * @return the map of available video filters
   */
  public Map getFilters() {
    return filterClasses;
  }

  /**
   * Returns true if mouse coordinates are displayed
   *
   * @return <code>true</code> if mouse coordinates are displayed
   */
  public boolean isShowCoordinates() {
    return showCoordinates;
  }

  /**
   * Hides the mouse box
   */
  public void hideMouseBox() {
    if (mousePanel.isVisible()) {
      mousePanel.setText(null);
    }
  }

  /**
   * Responds to property change events. VideoPanel listens for the following
   * events: "videoclip" and "stepnumber" from VideoPlayer, "coords" and "image"
   * from Video.
   *
   * @param e the property change event
   */
  public void propertyChange(PropertyChangeEvent e) {
    String name = e.getPropertyName();
    if (name.equals("size")) {                 // from Video //$NON-NLS-1$
    	Dimension dim = (Dimension)e.getNewValue();
      setImageWidth(dim.width);
      setImageHeight(dim.height);
    }
    else if (name.equals("coords")) {                 // from Video //$NON-NLS-1$
      // replace current coords with video's new coords
      coords = video.getCoords();
    }
    else if (name.equals("image") || //$NON-NLS-1$
             name.equals("videoVisible")) {      // from Video //$NON-NLS-1$
      repaint();
    }
    else if (name.equals("stepnumber")) {        // from VideoPlayer //$NON-NLS-1$
      repaint();
    }
    else if (name.equals("videoclip")) {         // from VideoPlayer //$NON-NLS-1$
      // update property change listeners
      VideoClip oldClip = (VideoClip)e.getOldValue();
      oldClip.removePropertyChangeListener("startframe", this); //$NON-NLS-1$
      oldClip.removePropertyChangeListener("stepsize", this); //$NON-NLS-1$
      oldClip.removePropertyChangeListener("stepcount", this); //$NON-NLS-1$
      oldClip.removePropertyChangeListener("starttime", this); //$NON-NLS-1$
      VideoClip clip = (VideoClip)e.getNewValue();
      clip.addPropertyChangeListener("startframe", this); //$NON-NLS-1$
      clip.addPropertyChangeListener("stepsize", this); //$NON-NLS-1$
      clip.addPropertyChangeListener("stepcount", this); //$NON-NLS-1$
      clip.addPropertyChangeListener("starttime", this); //$NON-NLS-1$
      // replace current video with new clip's video
      if (video != null) {
        video.removePropertyChangeListener("coords", this); //$NON-NLS-1$
        video.removePropertyChangeListener("image", this); //$NON-NLS-1$
        video.removePropertyChangeListener("videoVisible", this); //$NON-NLS-1$
        video.removePropertyChangeListener("size", this); //$NON-NLS-1$
        super.removeDrawable(video);
      }
      video = clip.getVideo();
      if (video != null) {
        video.addPropertyChangeListener("coords", this); //$NON-NLS-1$
        video.addPropertyChangeListener("image", this); //$NON-NLS-1$
        video.addPropertyChangeListener("videoVisible", this); //$NON-NLS-1$
        video.addPropertyChangeListener("size", this); //$NON-NLS-1$
        // synchronize coords
        if (video.isMeasured()) coords = video.getCoords();
        else video.setCoords(coords);
        drawableList.add(0, video); // put video at back
        BufferedImage vidImage = video.getImage();
        if (vidImage != null) {
          setImageWidth(vidImage.getWidth());
          setImageHeight(vidImage.getHeight());
        }
      }
      repaint();
    }
  }

  /**
   * Overrides DrawingPanel paintEverything method.
   *
   * @param g the graphics context to draw on
   */
  protected void paintEverything(Graphics g){
    // increase bottom gutter to make room for the player
    if (playerVisible) bottomGutter += player.height;
    super.paintEverything(g);
    // restore bottom gutter
    if (playerVisible) bottomGutter -= player.height;
  }

  /**
   * Overrides DrawingPanel scale method to handle drawing in imagespace
   *
   * @param drawables the list of drawable objects
   */
  protected void scale(ArrayList drawables) {
    if (drawingInImageSpace) {
      // scale to image units
      xminPreferred = -imageBorder*imageWidth + xOffset;
      xmaxPreferred = imageWidth + imageBorder*imageWidth + xOffset;
      yminPreferred = imageHeight + imageBorder*imageHeight + yOffset;
      ymaxPreferred = -imageBorder*imageHeight + yOffset;
    }
    super.scale(drawables);
  }

  /**
   * Overrides DrawingPanel checkImage method so offscreenImage will not include
   * the videoPlayer.
   *
   * @return <code>true</code> if the image is correctly sized
   */
  protected boolean checkImage(){
    Dimension d = getSize();
    if (playerVisible) d.height -= player.height; // don't include player area
    if (d.width <= 2 || d.height <= 2) return false; // image is too small
    if ((offscreenImage == null)
     || (d.width != offscreenImage.getWidth())
     || (d.height != offscreenImage.getHeight()))
      offscreenImage = new BufferedImage(d.width, d.height,
                                         BufferedImage.TYPE_INT_RGB);
    if (offscreenImage == null) return false;
    return true;  // the buffered image exists and is the correct size
  }

  /**
   * Gets the world coordinates of the last mouse event
   *
   * @return world coordinates of last mouse event
   */
  public Point2D getWorldMousePoint() {
    // get coordinates of mouse
    pt.setLocation(getMouseX(), getMouseY());
    // transform if nec
    if (isDrawingInImageSpace()) {
      int n = getFrameNumber();
      AffineTransform toWorld = getCoords().getToWorldTransform(n);
      toWorld.transform(pt, pt);
    }
    return pt;
  }

//______________________________ object loader ________________________________

  /**
   * Returns an XML.ObjectLoader to save and load data for this object.
   *
   * @return the object loader
   */
  public static XML.ObjectLoader getLoader() {
    return new Loader();
  }

  /**
   * A class to save and load data for this object.
   */
  static class Loader implements XML.ObjectLoader {

    /**
     * Saves object data to an XMLControl.
     *
     * @param control the control to save to
     * @param obj the object to save
     */
    public void saveObject(XMLControl control, Object obj) {
      VideoPanel vidPanel = (VideoPanel)obj;
      // save the video clip and coords
      control.setValue("videoclip", vidPanel.getPlayer().getVideoClip()); //$NON-NLS-1$
      control.setValue("coords", vidPanel.getCoords()); //$NON-NLS-1$
      // save the drawables
      ArrayList list = vidPanel.getDrawables();
      list.remove(vidPanel.getVideo()); // the video is saved by videoclip
      if (!list.isEmpty()) control.setValue("drawables", list); //$NON-NLS-1$
    }

    /**
     * Creates an object using data from an XMLControl.
     *
     * @param control the control
     * @return the newly created object
     */
    public Object createObject(XMLControl control){
      return new VideoPanel();
    }

    /**
     * Loads an object with data from an XMLControl.
     *
     * @param control the control
     * @param obj the object
     * @return the loaded object
     */
    public Object loadObject(XMLControl control, Object obj) {
      VideoPanel vidPanel = (VideoPanel)obj;
      // load the video clip
      VideoClip clip = (VideoClip) control.getObject("videoclip"); //$NON-NLS-1$
      if (clip != null) {
        vidPanel.getPlayer().setVideoClip(clip);
      }
      // load the coords
      vidPanel.setCoords((ImageCoordSystem)control.getObject("coords")); //$NON-NLS-1$
      // load the drawables
      Collection drawables = (Collection)control.getObject("drawables"); //$NON-NLS-1$
      if (drawables != null) {
        Iterator it = drawables.iterator();
        while (it.hasNext()) {
          vidPanel.addDrawable((Drawable)it.next());
        }
      }
      return obj;
    }
  }

}
